package com.touzhijia.authority.security;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ldap.core.ContextSource;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.*;
import org.springframework.security.core.userdetails.cache.NullUserCache;
import org.springframework.stereotype.Component;

/**
 * 自定义认证 UserLoginToken
 * <p>
 * 作者： lzw<br/>
 * 创建时间：2018-03-15 17:41 <br/>
 */
@Component
@Slf4j
public class UserLoginTokenAuthenticationProvider implements AuthenticationProvider {

    /**
     * 认证服务域名
     */
    private static final String LDAP_DOMAIN = "@touzhijia.net";

    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private ContextSource contextSource;

    private UserCache userCache = new NullUserCache();
    private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();
    private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();
    private boolean hideUserNotFoundExceptions = true;

    public UserLoginTokenAuthenticationProvider(SecurityConfig securityConfig) {
        if (securityConfig.getLogin().getHideUserNotFoundExceptions() != null) {
            hideUserNotFoundExceptions = securityConfig.getLogin().getHideUserNotFoundExceptions();
        }
    }

    /**
     * 认证用户登录
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        log.info("### 开始验证用户[{}]", authentication.toString());
        if (!(authentication instanceof UserLoginToken)) {
            throw new BadCredentialsException("不支持的登录信息");
        }
        UserLoginToken userLoginToken = (UserLoginToken) authentication;
        if (StringUtils.isBlank(userLoginToken.getUsername())) {
            throw new BadCredentialsException("用户名不能为空");
        }
        if (StringUtils.isBlank(userLoginToken.getPassword())) {
            throw new BadCredentialsException("密码不能为空");
        }
        // 查询帐号信息
        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(userLoginToken.getUsername());
        if (user == null) {
            cacheWasUsed = false;
            try {
                user = retrieveUser(userLoginToken.getUsername());
            } catch (UsernameNotFoundException notFound) {
                log.info("### 用户不存在[username={}]", userLoginToken.getUsername());
                if (hideUserNotFoundExceptions) {
                    throw new BadCredentialsException("用户名或密码错误");
                } else {
                    throw notFound;
                }
            }
            log.info("### 已查询到用户信息[{}]", user.toString());
        }
        // 验证帐号信息
        try {
            preAuthenticationChecks.check(user);
            additionalAuthenticationChecks(userLoginToken);
        } catch (AuthenticationException exception) {
            if (cacheWasUsed) {
                // 缓存中的数据不一定准确，如果使用缓存数据身份认证异常，则直接使用数据库的数据
                cacheWasUsed = false;
                user = retrieveUser(userLoginToken.getUsername());
                preAuthenticationChecks.check(user);
                additionalAuthenticationChecks(userLoginToken);
            } else {
                throw exception;
            }
        }
        postAuthenticationChecks.check(user);
        log.info("### 用户认证成功 [{}]", user.toString());
        // 帐号信息存入缓存
        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }
        // 返回认证成功的 Authentication
        return createSuccessAuthentication(userLoginToken, user);
    }

    /**
     * 是否支持验证此身份类型
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return (UserLoginToken.class.isAssignableFrom(authentication));
    }

    /**
     * 查询用户
     */
    private UserDetails retrieveUser(String username) {
        UserDetails loadedUser;
        try {
            loadedUser = userDetailsService.loadUserByUsername(username);
        } catch (UsernameNotFoundException notFound) {
            throw notFound;
        } catch (Exception repositoryProblem) {
            throw new InternalAuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem);
        }
        if (loadedUser == null) {
            throw new InternalAuthenticationServiceException("验证服务内部异常");
        }
        return loadedUser;
    }

    /**
     * 真正的密码验证
     */
    private void additionalAuthenticationChecks(UserLoginToken userLoginToken) throws AuthenticationException {
        if (userLoginToken.getUsername() == null) {
            throw new BadCredentialsException("用户名不能为空");
        }
        // TODO 处理用户密码

        // TODO 验证自定义的用户登录

        String password = userLoginToken.getPassword();
        // 验证用户登录
        try {
            contextSource.getContext(userLoginToken.getUsername() + LDAP_DOMAIN, password).close();
        } catch (Throwable e) {
            log.info("### 用户名密码验证失败 [{}]", userLoginToken.toString());
            throw new BadCredentialsException("用户名密码验证失败", e);
        }
        log.info("### 用户名密码验证成功 [{}]", userLoginToken.toString());
    }

    /**
     * 创建认证成功的Authentication
     */
    private Authentication createSuccessAuthentication(UserLoginToken userLoginToken, UserDetails user) {
        UserLoginToken result = new UserLoginToken(user);
        result.setDetails(userLoginToken.getDetails());
        return result;
    }

    @Slf4j
    private static class DefaultPreAuthenticationChecks implements UserDetailsChecker {
        public void check(UserDetails user) {
            if (!user.isAccountNonLocked()) {
                log.info("帐号已锁定 [username={}]", user.getUsername());
                throw new LockedException("帐号已锁定");
            }
            if (!user.isEnabled()) {
                log.info("帐号已禁用 [username={}]", user.getUsername());
                throw new DisabledException("帐号已禁用");
            }
            if (!user.isAccountNonExpired()) {
                log.info("帐号已过期 [username={}]", user.getUsername());
                throw new AccountExpiredException("帐号已过期");
            }
        }
    }

    @Slf4j
    private static class DefaultPostAuthenticationChecks implements UserDetailsChecker {
        public void check(UserDetails user) {
            if (!user.isCredentialsNonExpired()) {
                log.info("帐号密码已过期 [username={}]", user.getUsername());
                throw new CredentialsExpiredException("帐号密码已过期");
            }
        }
    }
}
